/*
 * Created on Feb 26, 2005
 */
package org.flexdock.view;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.LayoutManager2;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPanel;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.DockingStrategy;
import org.flexdock.docking.defaults.DefaultDockingStrategy;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.event.DockingListener;
import org.flexdock.docking.props.DockablePropertySet;
import org.flexdock.docking.props.PropertyManager;
import org.flexdock.plaf.PlafManager;
import org.flexdock.plaf.theme.ViewUI;
import org.flexdock.util.DockingUtility;
import org.flexdock.util.ResourceManager;
import org.flexdock.util.SwingUtility;

/**
 * The {@code View} class is slightly incompatible with {@code JComponent}.
 * Similar to JFC/Swing top-level containers, a {@code View} contains only a
 * content pane {@code Container} and a {@code Titlebar}. The <b>content pane</b>
 * should contain all the components displayed by the {@code View}. As a
 * convenience {@code add} and its variants, {@code remove(Component)} and
 * {@code setLayout} have been overridden to forward to the
 * {@code contentPane} as necessary. This means you can write:
 * <p>
 * <pre>
 * view.add(child);
 * </pre>
 * <p>
 * And the child will be added to the contentPane. The content pane will always
 * be non-null. Attempting to set it to null will cause the View to throw an
 * exception. The default content pane will not have a layout manager set.
 *
 * @author Christopher Butler
 * @author Karl Schaefer
 * @see javax.swing.JFrame
 * @see javax.swing.JRootPane
 */
public class View extends JComponent implements Dockable, DockingConstants {
	protected class ViewLayout implements LayoutManager2, Serializable {
		/**
		 * Returns the amount of space the layout would like to have.
		 *
		 * @param parent the Container for which this layout manager is being used
		 * @return a Dimension object containing the layout's preferred size
		 */
		public Dimension preferredLayoutSize(Container parent) {
			Dimension rd, tpd;
			Insets i = getInsets();

			if (contentPane != null) {
				rd = contentPane.getPreferredSize();
			} else {
				rd = parent.getSize();
			}
			if (titlepane != null && titlepane.isVisible()) {
				tpd = titlepane.getPreferredSize();
			} else {
				tpd = new Dimension(0, 0);
			}
			return new Dimension(Math.max(rd.width, tpd.width) + i.left
					+ i.right, rd.height + tpd.height + i.top + i.bottom);
		}

		/**
		 * Returns the minimum amount of space the layout needs.
		 *
		 * @param parent the Container for which this layout manager is being used
		 * @return a Dimension object containing the layout's minimum size
		 */
		public Dimension minimumLayoutSize(Container parent) {
			Dimension rd, tpd;
			Insets i = getInsets();
			if (contentPane != null) {
				rd = contentPane.getMinimumSize();
			} else {
				rd = parent.getSize();
			}
			if (titlepane != null && titlepane.isVisible()) {
				tpd = titlepane.getMinimumSize();
			} else {
				tpd = new Dimension(0, 0);
			}
			return new Dimension(Math.max(rd.width, tpd.width) + i.left
					+ i.right, rd.height + tpd.height + i.top + i.bottom);
		}

		/**
		 * Returns the maximum amount of space the layout can use.
		 *
		 * @param target the Container for which this layout manager is being used
		 * @return a Dimension object containing the layout's maximum size
		 */
		public Dimension maximumLayoutSize(Container target) {
			Dimension rd, tpd;
			Insets i = getInsets();
			if (titlepane != null && titlepane.isVisible()) {
				tpd = titlepane.getMaximumSize();
			} else {
				tpd = new Dimension(0, 0);
			}
			if (contentPane != null) {
				rd = contentPane.getMaximumSize();
			} else {
				// This is silly, but should stop an overflow error
				rd = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE - i.top
						- i.bottom - tpd.height - 1);
			}
			return new Dimension(Math.min(rd.width, tpd.width) + i.left
					+ i.right, rd.height + tpd.height + i.top + i.bottom);
		}

		/**
		 * Instructs the layout manager to perform the layout for the specified
		 * container.
		 *
		 * @param parent the Container for which this layout manager is being used
		 */
		public void layoutContainer(Container parent) {
			Rectangle b = parent.getBounds();
			Insets i = getInsets();
			int contentY = 0;
			int w = b.width - i.right - i.left;
			int h = b.height - i.top - i.bottom;

			if (titlepane != null && titlepane.isVisible()) {
				Dimension mbd = titlepane.getPreferredSize();
				titlepane.setBounds(0, 0, w, mbd.height);
				contentY += mbd.height;
			}
			if (contentPane != null) {
				contentPane.setBounds(0, contentY, w, h - contentY);
			}
		}

		public void addLayoutComponent(String name, Component comp) {
		}

		public void removeLayoutComponent(Component comp) {
		}

		public void addLayoutComponent(Component comp, Object constraints) {
		}

		public float getLayoutAlignmentX(Container target) {
			return 0.0f;
		}

		public float getLayoutAlignmentY(Container target) {
			return 0.0f;
		}

		public void invalidateLayout(Container target) {
		}
	}

	public static final String uiClassID = "Flexdock.view";

	public static final String ACTION_TOGGLE_NEXT = "toggleNextView";
	public static final String ACTION_TOGGLE_PREVIOUS = "togglePreviousView";

	static final DockingStrategy VIEW_DOCKING_STRATEGY = createDockingStrategy();

	private String persistentId;

	protected Titlebar titlepane;

	protected Container contentPane;

	protected boolean contentPaneCheckingEnabled;

	protected ArrayList dockingListeners;

	// protected boolean active;
	protected ArrayList dragSources;

	protected HashSet frameDragSources;

	private transient HashSet blockedActions;

	static {
		DockingManager.setDockingStrategy(View.class, VIEW_DOCKING_STRATEGY);
		PropertyManager.setDockablePropertyType(View.class, ViewProps.class);
	}

	public View(String persistentId) {
		this(persistentId, null);
	}

	public View(String persistentId, String title) {
		this(persistentId, title, null);
	}

	public View(String persistentId, String title, String tabText) {
		if (persistentId == null)
			throw new IllegalArgumentException(
					"The 'persistentId' parameter cannot be null.");

		this.persistentId = persistentId;

		dragSources = new ArrayList(1);
		frameDragSources = new HashSet(1);
		dockingListeners = new ArrayList(1);

		setContentPane(createContentPane());
		setTitlebar(createTitlebar());
		setLayout(createLayout());
		setContentPaneCheckingEnabled(true);

		if (title == null)
			title = "";
		setTitle(title);

		if (tabText == null)
			tabText = title;
		setTabText(tabText);

		addHierarchyListener(new HierarchyListener() {
			public void hierarchyChanged(HierarchyEvent e) {
				clearButtonRollovers();
			}
		});

		updateUI();

		DockingManager.registerDockable((Dockable) this);

		getActionMap().put(ACTION_TOGGLE_NEXT, new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				SwingUtility.toggleFocus(+1);
			}
		});
		getActionMap().put(ACTION_TOGGLE_PREVIOUS, new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				SwingUtility.toggleFocus(-1);
			}
		});
	}

	private static DockingStrategy createDockingStrategy() {
		return new DefaultDockingStrategy() {
			protected DockingPort createDockingPortImpl(DockingPort base) {
				return new Viewport();
			}
		};
	}

	public static View getInstance(String viewId) {
		Dockable view = DockingManager.getDockable(viewId);
		return view instanceof View ? (View) view : null;
	}

	protected Container createContentPane() {
		return new JPanel();
	}

	protected LayoutManager createLayout() {
		return new ViewLayout();
	}

	protected Titlebar createTitlebar() {
		Titlebar t = new Titlebar();
		t.setFont(this.getFont());
		t.setView(this);

		return t;
	}

	public Container getContentPane() {
		return contentPane;
	}

	public Titlebar getTitlebar() {
		return titlepane;
	}

	public DockablePropertySet getDockingProperties() {
		return PropertyManager.getDockablePropertySet(this);
	}

	public ViewProps getViewProperties() {
		return (ViewProps) getDockingProperties();
	}

	public void addAction(Action action) {
		if (titlepane != null)
			titlepane.addAction(action);
	}

	public void addAction(String action) {
		if (titlepane != null)
			titlepane.addAction(action);
	}

	public void removeActions() {
		if (titlepane != null)
			titlepane.removeAllActions();
	}

	public void setIcon(Icon icon) {
		if (titlepane != null)
			titlepane.setIcon(icon);
	}

	public void setIcon(String imgUri) {
		Icon icon = imgUri == null ? null : ResourceManager.createIcon(imgUri);
		setIcon(icon);
	}

	/**
	 * Sets the content pane for this view.
	 *
	 * @param c the container to use as the content pane.
	 * @throws IllegalArgumentException if {@code c} is {@code null} or if {@code c} is the
	 *                                  {@code titlepane}.
	 * @see #titlepane
	 * @see #getTitlePane()
	 */
	public void setContentPane(Container c) throws IllegalArgumentException {
		if (c == null)
			throw new IllegalArgumentException(
					"Unable to set a null content pane.");
		if (c == titlepane)
			throw new IllegalArgumentException(
					"Cannot use the same component as both content pane and titlebar.");

		if (contentPane != null) {
			remove(contentPane);
		}
		contentPane = c;
		if (contentPane != null) {
			boolean checkingEnabled = isContentPaneCheckingEnabled();
			try {
				setContentPaneCheckingEnabled(false);
				add(contentPane);
			} finally {
				setContentPaneCheckingEnabled(checkingEnabled);
			}
		}
	}

	protected String getPreferredTitlebarUIName() {
		return ui instanceof ViewUI ? ((ViewUI) ui).getPreferredTitlebarUI()
				: null;
	}

	public void setTitlebar(Titlebar titlebar) {
		if (titlebar != null && titlebar == contentPane) {
			throw new IllegalArgumentException(
					"Cannot use the same component as both content pane and titlebar.");
		}
		if (titlepane != null) {
			remove(titlepane);
			titlepane.setView(null);
			dragSources.remove(titlepane);
			frameDragSources.remove(titlepane);
			DockingManager.removeDragListeners(titlepane);
		}
		titlepane = titlebar;
		if (titlepane != null) {
			boolean checkingEnabled = isContentPaneCheckingEnabled();
			try {
				setContentPaneCheckingEnabled(false);
				add(titlepane);
			} finally {
				setContentPaneCheckingEnabled(checkingEnabled);
			}

			dragSources.add(titlepane);
			frameDragSources.add(titlepane);
			DockingManager.updateDragListeners(this);
		}

	}

	protected Component getTitlePane() {
		return titlepane;
	}

	public void setTitle(String title) {
		if (titlepane != null) {
			titlepane.setText(title);
		}
	}

	public void setTitle(String title, boolean alsoTabText) {
		setTitle(title);

		if (alsoTabText) {
			setTabText(title);
		}
	}

	public String getTitle() {
		Titlebar tbar = getTitlebar();
		return tbar == null ? null : tbar.getText();
	}

	public void updateUI() {
		setUI(PlafManager.getUI(this));
	}

	public String getUIClassID() {
		return uiClassID;
	}

	/**
	 * {@inheritDoc}
	 */
	protected void addImpl(Component comp, Object constraints, int index) {
		if (comp instanceof Titlebar)
			((Titlebar) comp).setView(this);

		if (isContentPaneCheckingEnabled()) {
			getContentPane().add(comp, constraints, index);
		} else {
			super.addImpl(comp, constraints, index);
		}
	}

	public void remove(Component comp) {
		if (comp == contentPane) {
			super.remove(comp);
		} else {
			getContentPane().remove(comp);
		}
	}

	public AbstractButton getActionButton(String actionName) {
		Titlebar tbar = getTitlebar();
		return tbar == null ? null : tbar.getActionButton(actionName);
	}

	public Component getComponent() {
		return this;
	}

	public List getDragSources() {
		return dragSources;
	}

	public Set getFrameDragSources() {
		return frameDragSources;
	}

	public String getPersistentId() {
		return persistentId;
	}

	public boolean isTerritoryBlocked(Dockable dockable, String region) {
		return getDockingProperties().isTerritoryBlocked(region).booleanValue();
	}

	public void setTerritoryBlocked(String region, boolean blocked) {
		getDockingProperties().setTerritoryBlocked(region, blocked);
	}

	public String getTabText() {
		String txt = getDockingProperties().getDockableDesc();
		return txt == null ? getTitle() : txt;
	}

	public void setTabText(String tabText) {
		getDockingProperties().setDockableDesc(tabText);
	}

	public Icon getTabIcon() {
		return getDockingProperties().getTabIcon();
	}

	public void setTabIcon(Icon icon) {
		getDockingProperties().setTabIcon(icon);
	}

	public boolean dock(Dockable dockable) {
		return dock(dockable, CENTER_REGION);
	}

	public DockingPort getDockingPort() {
		return DockingManager.getDockingPort((Dockable) this);
	}

	public Dockable getSibling(String region) {
		return DefaultDockingStrategy.getSibling(this, region);
	}

	public Viewport getViewport() {
		DockingPort port = getDockingPort();
		return port instanceof Viewport ? (Viewport) port : null;
	}

	public boolean dock(Dockable dockable, String relativeRegion) {
		return DockingManager.dock(dockable, this, relativeRegion);
	}

	public boolean dock(Dockable dockable, String relativeRegion, float ratio) {
		return DockingManager.dock(dockable, this, relativeRegion, ratio);
	}

	public void setActive(boolean b) {
		getViewProperties().setActive(b);
	}

	public boolean isActive() {
		return getViewProperties().isActive().booleanValue();
	}

	public void setActiveStateLocked(boolean b) {
		getViewProperties().setActiveStateLocked(b);
	}

	public boolean isActiveStateLocked() {
		return getViewProperties().isActiveStateLocked().booleanValue();
	}

	public boolean isMinimized() {
		return DockingUtility.isMinimized(this);
	}

	public int getMinimizedConstraint() {
		return DockingUtility.getMinimizedConstraint(this);
	}

	public void addDockingListener(DockingListener listener) {
		dockingListeners.add(listener);
	}

	public DockingListener[] getDockingListeners() {
		return (DockingListener[]) dockingListeners
				.toArray(new DockingListener[0]);
	}

	public void removeDockingListener(DockingListener listener) {
		dockingListeners.remove(listener);
	}

	public void dockingCanceled(DockingEvent evt) {
	}

	public void dockingComplete(DockingEvent evt) {
		setActionBlocked(DockingConstants.PIN_ACTION, isFloating());
		if (titlepane != null)
			titlepane.revalidate();
	}

	public void dragStarted(DockingEvent evt) {
	}

	public void dropStarted(DockingEvent evt) {
	}

	public void undockingComplete(DockingEvent evt) {
		clearButtonRollovers();
	}

	public void undockingStarted(DockingEvent evt) {
	}

	private void clearButtonRollovers() {
		if (titlepane == null)
			return;

		Component[] comps = titlepane.getComponents();
		for (int i = 0; i < comps.length; i++) {
			Button button = comps[i] instanceof Button ? (Button) comps[i]
					: null;
			if (button != null) {
				button.getModel().setRollover(false);
			}
		}
	}

	public void setActionBlocked(String actionName, boolean blocked) {
		if (actionName == null)
			return;

		Set actions = getBlockedActions();
		if (blocked)
			actions.add(actionName);
		else {
			if (actions != null)
				actions.remove(actionName);
		}
	}

	public boolean isActionBlocked(String actionName) {
		return actionName == null || blockedActions == null ? false
				: blockedActions.contains(actionName);
	}

	private HashSet getBlockedActions() {
		if (blockedActions == null)
			blockedActions = new HashSet(1);
		return blockedActions;
	}

	public boolean isFloating() {
		return DockingUtility.isFloating(this);
	}

	protected String paramString() {
		return "id=" + getPersistentId() + "," + super.paramString();
	}

	/**
	 * @return the contentPaneCheckingEnabled
	 */
	protected boolean isContentPaneCheckingEnabled() {
		return contentPaneCheckingEnabled;
	}

	/**
	 * @param contentPaneCheckingEnabled the contentPaneCheckingEnabled to set
	 */
	protected void setContentPaneCheckingEnabled(
			boolean contentPaneCheckingEnabled) {
		this.contentPaneCheckingEnabled = contentPaneCheckingEnabled;
	}

	/**
	 * Sets the <code>LayoutManager</code>. Overridden to conditionally
	 * forward the call to the <code>contentPane</code>.
	 *
	 * @param manager the <code>LayoutManager</code>
	 * @see #setContentPaneCheckingEnabled
	 */
	public void setLayout(LayoutManager manager) {
		if (isContentPaneCheckingEnabled()) {
			getContentPane().setLayout(manager);
		} else {
			super.setLayout(manager);
		}
	}
}
